package com.example.diffservice.algorithm;

import com.example.common.constant.Constants;
import com.example.common.entity.ChangedFile;
import com.example.common.utils.GithubApiUtils;
import com.example.diffservice.algorithm.base.common.Global;
import com.example.diffservice.algorithm.entity.CommitFile;
import com.example.diffservice.algorithm.entity.Meta;
import com.example.diffservice.algorithm.util.FileRWUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

/**
 * @author DestinyCc
 * @date 2022/3/28
 */
@Slf4j
public class MyDiff {

    public RevWalk revWalk;

    public Repository repository;

    public Git git;

    public Meta meta;

    public void run(String author, String pullNumber, String lastCommitId, List<String> parentId, String repo, Integer tag) throws Exception{
        Global.changeEntityId = 0;
        meta = Meta.builder()
                .author(author)
                .commitHash(lastCommitId)
                .parents(parentId)
                .outputDir(Constants.DEFAULT_OUTPUT_DIR + repo + "/" + pullNumber + "/")
                .build();
        FileRepositoryBuilder builder = new FileRepositoryBuilder();
        File file = new File(Constants.DEFAULT_PROJECT_LOCATION + "/" + author + "/"+ repo);
        if (! file.exists()){
            //todo
//            GitUtils.clone(author, repo);
            repository = builder.setGitDir(new File(Constants.DEFAULT_PROJECT_LOCATION + "/" + author + "/"+ repo + "/.git")).readEnvironment()
                .findGitDir() // scan up the file system tree
                .build();
            revWalk = new RevWalk(repository);
            git = new Git(repository);
        }else {
            //todo
//            GitUtils.pull(author, repo);
            repository = builder.setGitDir(new File(Constants.DEFAULT_PROJECT_LOCATION + "/" + author + "/"+ repo + "/.git")).readEnvironment()
                    .findGitDir() // scan up the file system tree
                    .build();
            revWalk = new RevWalk(repository);
            git = new Git(repository);
        }
        this.analyse(lastCommitId, parentId, repo);
        Generator generator = new Generator(this.meta);
        generator.generateResult();
    }

    private void analyse(String lastCommitId, List<String> parentId, String repo) throws Exception {
        ObjectId commitId = ObjectId.fromString(lastCommitId);
        RevCommit lastCommit = revWalk.parseCommit(commitId);
        Map<String, List<DiffEntry>> changedFiles = this.getDiffEntry(lastCommit, parentId);
        this.handleCommit(changedFiles, lastCommitId, lastCommit);
    }

    private Map<String, List<DiffEntry>> getDiffEntry(RevCommit commit, List<String> parentId) throws Exception {
        Map<String, List<DiffEntry>> result = new HashMap<>();
        for (String parent: parentId){
            ObjectReader reader = git.getRepository().newObjectReader();
            CanonicalTreeParser newTreeIter = new CanonicalTreeParser();
            ObjectId newTree = commit.getTree().getId();
            newTreeIter.reset(reader, newTree);
            CanonicalTreeParser oldTreeIter = new CanonicalTreeParser();
            RevCommit parentCommit = revWalk.parseCommit(ObjectId.fromString(parent));
            ObjectId oldTree = parentCommit.getTree().getId();
            oldTreeIter.reset(reader, oldTree);
            DiffFormatter diffFormatter = new DiffFormatter(DisabledOutputStream.INSTANCE);
            diffFormatter.setRepository(git.getRepository());
            List<DiffEntry> entries = diffFormatter.scan(oldTree, newTree);
            result.put(parentCommit.getName(), entries);
            diffFormatter.close();
        }
        return result;
    }

    private void handleCommit(Map<String, List<DiffEntry>> changedFiles, String commitId, RevCommit commit) throws Exception{
        int count = 0;
        for (Map.Entry<String, List<DiffEntry>> entry: changedFiles.entrySet()){
            String parentCommitId = entry.getKey();
            meta.addParentCommit(parentCommitId);
            List<DiffEntry> changedFileEntry = entry.getValue();
            for (DiffEntry diffEntry: changedFileEntry){
                if (diffEntry.getChangeType() == DiffEntry.ChangeType.MODIFY){
                    String file = diffEntry.getNewPath();
                    boolean isFiltered = isFilter(file);
                    setCommitFile(count, parentCommitId, commitId, file, isFiltered);
                    meta.addAction("modified");
                    count++;
                }else if (diffEntry.getChangeType() == DiffEntry.ChangeType.ADD){
                    String file = diffEntry.getNewPath();
                    boolean isFiltered = isFilter(file);
                    setAddCommitFile(count, parentCommitId, commitId, file, isFiltered);
                    meta.addAction("added");
                    count++;
                }else {
                    String file = diffEntry.getOldPath();
                    boolean isFiltered = isFilter(file);
                    setDeleteCommitFile(count, parentCommitId, commitId, file, isFiltered);
                    meta.addAction("removed");
                    count++;
                }
            }
        }
    }

    /**
     * 过滤非java文件与test文件
     * @param filePath
     * @return
     */
    public static boolean isFilter(String filePath){
        String name = filePath.toLowerCase(Locale.ROOT);
        if (!name.endsWith(".java")){
            return true;
        }
        if (name.contains("\\test\\") || name.contains("/test/")){
            return true;
        }
        String[] split = filePath.split("/");
        String filename = split[split.length-1];
        if (filename.endsWith("Test.java") || filename.startsWith("Test")){
            return true;
        }
        return false;
    }

    private void setCommitFile(int cnt,String parentCommitId,String commitId,String filePath,boolean isFiltered) throws Exception{
        CommitFile commitFile = new CommitFile();
        byte[] prevFile = this.extract(filePath, parentCommitId);
        byte[] currFile = this.extract(filePath, commitId);
        Path prevFilePath = Paths.get(meta.getOutputDir() + "/prev/" + filePath);
        Path currFilePath = Paths.get(meta.getOutputDir() + "/curr/" + filePath);
        if (!prevFilePath.toFile().exists() || !currFilePath.toFile().exists()){
            int index2 = prevFilePath.toFile().getAbsolutePath().lastIndexOf(File.separator);
            String prevDir = prevFilePath.toFile().getAbsolutePath().substring(0, index2);
            index2 = currFilePath.toFile().getAbsolutePath().lastIndexOf(File.separator);
            String currDir = currFilePath.toFile().getAbsolutePath().substring(0, index2);
            File prev = new File(prevDir);
            File curr = new File(currDir);
            if (!prev.exists()){
                prev.mkdirs();
            }
            if (!curr.exists()){
                curr.mkdirs();
            }
        }
        Files.write(prevFilePath, prevFile);
        Files.write(currFilePath, currFile);
        int index = filePath.lastIndexOf("/");
        String filename = filePath.substring(index+1);
        commitFile.setId(cnt);
        commitFile.setPrevFilePath("prev/" + filePath);
        commitFile.setCurrFilePath("curr/" + filePath);
        if (!isFiltered){
            commitFile.setDiffPath(meta.getOutputDir() + "/gen/Diff" + filename + ".json");
        }
        commitFile.setFileName(filename);
        commitFile.setParentCommit(parentCommitId);
        meta.addFile(commitFile);
    }

    private void setAddCommitFile(int cnt,String parentCommitId,String commitId,String filePath,boolean isFiltered) throws Exception{
        CommitFile commitFile = new CommitFile();
        byte[] currFile = this.extract(filePath, commitId);
        Path currFilePath = Paths.get(meta.getOutputDir() + "/curr/" + filePath);
        if (!currFilePath.toFile().exists()){
            int index2 = currFilePath.toFile().getAbsolutePath().lastIndexOf(File.separator);
            String currDir = currFilePath.toFile().getAbsolutePath().substring(0, index2);
            File curr = new File(currDir);
            if (!curr.exists()){
                curr.mkdirs();
            }
        }
        Files.write(currFilePath, currFile);
        int index = filePath.lastIndexOf("/");
        String filename = filePath.substring(index+1);
        commitFile.setId(cnt);
        commitFile.setCurrFilePath("curr/" + filePath);
        if (!isFiltered){
            commitFile.setDiffPath(meta.getOutputDir() + "/gen/Diff" + filename + ".json");
        }
        commitFile.setFileName(filename);
        commitFile.setParentCommit(parentCommitId);
        meta.addFile(commitFile);
    }

    private void setDeleteCommitFile(int cnt,String parentCommitId,String commitId,String filePath,boolean isFiltered) throws Exception{
        CommitFile commitFile = new CommitFile();
        byte[] prevFile = this.extract(filePath, parentCommitId);
        Path prevFilePath = Paths.get(meta.getOutputDir() + "/prev/" + filePath);
        if (!prevFilePath.toFile().exists()){
            int index2 = prevFilePath.toFile().getAbsolutePath().lastIndexOf(File.separator);
            String prevDir = prevFilePath.toFile().getAbsolutePath().substring(0, index2);
            File prev = new File(prevDir);
            if (!prev.exists()){
                prev.mkdirs();
            }
        }
        Files.write(prevFilePath, prevFile);
        int index = filePath.lastIndexOf("/");
        String filename = filePath.substring(index+1);
        commitFile.setId(cnt);
        commitFile.setPrevFilePath("prev/" + filePath);
        if (!isFiltered){
            commitFile.setDiffPath(meta.getOutputDir() + "/gen/Diff" + filename + ".json");
        }
        commitFile.setFileName(filename);
        commitFile.setParentCommit(parentCommitId);
        meta.addFile(commitFile);
    }

    private byte[] extract(String filename, String revisionId) throws Exception{
        RevWalk walk = new RevWalk(repository);
        ObjectId objectId = repository.resolve(revisionId);
        RevCommit commit = walk.parseCommit(objectId);
        RevTree tree = commit.getTree();
        TreeWalk treeWalk = TreeWalk.forPath(repository, filename, tree);
        ObjectId id = treeWalk.getObjectId(0);
        InputStream inputStream = FileRWUtil.open(id, repository);
        byte[] res = FileRWUtil.toByteArray(inputStream);
        return res;
    }

    @SneakyThrows
    public void newRun(String owner, String author, String pullNumber, String lastCommitId, List<String> parentId, String repo, Integer tag){
        Global.changeEntityId = 0;
        meta = Meta.builder()
                .author(author)
                .commitHash(lastCommitId)
                .parents(parentId)
                .outputDir(Constants.DEFAULT_OUTPUT_DIR + repo + "/" + pullNumber + "/")
                .build();
        for (String parent: parentId){
            this.extractChangedFiles(owner, repo, pullNumber, parent);
        }
        Generator generator = new Generator(this.meta);
        generator.generateResult();
    }

    private void extractChangedFiles(String owner, String repo, String pullNumber, String parentId) {
        int count = 0;
        String requestUrl = String.format(Constants.GITHUB.LIST_PULL_REQUESTS_FILES, owner, repo, pullNumber);
        List<ChangedFile> changedFiles = new ArrayList<>();
        for (int i = 1; i < Integer.MAX_VALUE; i++) {
            UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(requestUrl);
            builder.queryParam("page", i);
            String uri = builder.build(true).toUriString();
            List<LinkedHashMap<String, Object>> files = (List<LinkedHashMap<String, Object>>) GithubApiUtils.callGithubApi(uri);
            if (files.size() == 0){
                break;
            }
            files.stream().map(f-> new ChangedFile((String) f.get("filename"),
                    (String) f.get("raw_url"),
                    (String) f.get("status"))).filter(Objects::nonNull).forEach(changedFiles::add);
        }
        for (ChangedFile changedFile: changedFiles){
            String filename = changedFile.getFilename();
            String rawUrl = changedFile.getRawUrl();
            String status = changedFile.getStatus();
            //"https://github.com/%s/%s/raw/%s/%s"
            //"https://github.com/spring-projects/spring-framework/raw/a05dc97c74b20091c3905beaa0933991ad4133a2/spring-beans%2Fsrc%2Fmain%2Fjava%2Forg%2Fspringframework%2Fbeans%2FAbstractNestablePropertyAccessor.java"
            boolean isFiltered = isFilter(filename);
            switch (status){
                case "modified":
                    String oldFileUrl = String.format(Constants.GITHUB.RAW_FILE, owner, repo, parentId, filename);
                    UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(oldFileUrl);
                    String oldFile = builder.build(true).toUriString();
//                    log.info("oldFileUrl = {}", oldFile);
                    fileDownload(oldFile, filename, pullNumber, repo, "prev");
//                    log.info("newFileUrl = {}", rawUrl);
                    fileDownload(rawUrl, filename, pullNumber, repo, "curr");
                    setModified(count, parentId, filename, isFiltered);
                    meta.addAction(status);
                    count++;
                    break;
                case "added":
//                    log.info("newFileUrl = {}", rawUrl);
                    fileDownload(rawUrl, filename, pullNumber, repo, "curr");
                    setAdded(count, parentId, filename, isFiltered);
                    meta.addAction(status);
                    count++;
                    break;
                case "removed":
//                    log.info("oldFileUrl = {}", rawUrl);
                    fileDownload(rawUrl, filename, pullNumber, repo, "prev");
                    setRemoved(count, parentId, filename, isFiltered);
                    meta.addAction(status);
                    count++;
                    break;
                default:
            }
        }
    }

    @SneakyThrows
    public void fileDownload(String url, String filename, String pullNumber, String repo, String base){
        URL u = new URL(url.replace(Constants.GITHUB.GITHUB, Constants.GITHUB.RAW).replace(Constants.GITHUB.RAW_STR, ""));
//        log.info(u.toString());
        //https://raw.githubusercontent.com/spring-projects/spring-framework/a05dc97c74b20091c3905beaa0933991ad4133a2/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java
        //https://github.com/spring-projects/spring-framework/raw/a05dc97c74b20091c3905beaa0933991ad4133a2/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java
        BufferedReader reader = new BufferedReader(new InputStreamReader(u.openStream()));
        StringBuilder stringBuilder = new StringBuilder();
        String temp;
        while ((temp = reader.readLine()) != null){
            stringBuilder.append(temp).append("\n");
        }
        String fileContent = stringBuilder.substring(0, stringBuilder.lastIndexOf("\n"));
        String fileParentDir = "";
        int index = filename.lastIndexOf("/");
        if (index != -1){
            fileParentDir = filename.substring(0, index);
        }
//        log.info("fileParentDir = {}", fileParentDir);
        String baseDir = Constants.DEFAULT_OUTPUT_DIR + "/" + repo + "/" + pullNumber + "/" + base + "/" + fileParentDir + "/";
        File dir = new File(baseDir);
        if (! dir.exists()){
            dir.mkdirs();
        }
        File file = new File(baseDir + filename.substring(filename.lastIndexOf("/")+1));
        if (file.exists()) {
            file.delete();
        }
        file.createNewFile();
        FileOutputStream fileOutputStream = new FileOutputStream(file, true);
        fileOutputStream.write(fileContent.getBytes(StandardCharsets.UTF_8));
        fileOutputStream.close();
    }

    @SneakyThrows
    private void setModified(int cnt,String parentCommitId,String filePath,boolean isFiltered){
        CommitFile commitFile = new CommitFile();
        int index = filePath.lastIndexOf("/");
        String filename = filePath.substring(index+1);
        commitFile.setId(cnt);
        commitFile.setPrevFilePath("prev/" + filePath);
        commitFile.setCurrFilePath("curr/" + filePath);
        if (!isFiltered){
//            commitFile.setDiffPath(meta.getOutputDir() + "/gen/Diff" + filename + ".json");
            commitFile.setDiffPath(meta.getOutputDir() + "/gen/Diff" + filePath.replace("/", "_") + ".json");
        }
        //        commitFile.setFileName(filename);
        commitFile.setFileName(filePath);
        commitFile.setParentCommit(parentCommitId);
        meta.addFile(commitFile);
    }

    @SneakyThrows
    private void setAdded(int cnt,String parentCommitId,String filePath,boolean isFiltered) {
        CommitFile commitFile = new CommitFile();
        int index = filePath.lastIndexOf("/");
        String filename = filePath.substring(index+1);
        commitFile.setId(cnt);
        commitFile.setCurrFilePath("curr/" + filePath);
        if (!isFiltered){
//            commitFile.setDiffPath(meta.getOutputDir() + "/gen/Diff" + filename + ".json");
            commitFile.setDiffPath(meta.getOutputDir() + "/gen/Diff" + filePath.replace("/", "_") + ".json");
        }
//        commitFile.setFileName(filename);
        commitFile.setFileName(filePath);
        commitFile.setParentCommit(parentCommitId);
        meta.addFile(commitFile);
    }

    @SneakyThrows
    private void setRemoved(int cnt,String parentCommitId,String filePath,boolean isFiltered) {
        CommitFile commitFile = new CommitFile();
        int index = filePath.lastIndexOf("/");
        String filename = filePath.substring(index+1);
        commitFile.setId(cnt);
        commitFile.setPrevFilePath("prev/" + filePath);
        if (!isFiltered){
//            commitFile.setDiffPath(meta.getOutputDir() + "/gen/Diff" + filename + ".json");
            commitFile.setDiffPath(meta.getOutputDir() + "/gen/Diff" + filePath.replace("/", "_") + ".json");

        }
        //        commitFile.setFileName(filename);
        commitFile.setFileName(filePath);
        commitFile.setParentCommit(parentCommitId);
        meta.addFile(commitFile);
    }


}

